/**
 *    Copyright (C) 2007-2009 DIVIS GmbH (http://www.divis-hagen.de/)
 * 
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation; either
 *    version 3 of the License, or (at your option) any later version.
 * 
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 * 
 *    You should have received a copy of the GNU Lesser General
 *    Public License along with this library; if not, write to the
 *    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 *    Boston, MA 02110-1301 USA
 */

package de.divis_hagen.json;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Date;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlTransient;


/**
 *
 * @author Aike J Sommer
 */
public class TreeBuilder {

    private JSONContextImpl context;

    private int depth;

    public TreeBuilder(JSONContextImpl context) {
        this.context = context;
    }

    public JSONTree build(Object o) throws JSONException {
        if (o == null) {
            throw new IllegalArgumentException();
        }

        JSONTree tree = new JSONTree();
        depth = 0;
        tree.setRoot(buildSub(context.meta(o.getClass()), o));

        return tree;
    }

//    public JSONTree build(Class type) throws JSONException {
//        JSONTree tree = new JSONTree();
//        depth = 0;
//        tree.setRoot(buildSub(type, null, true));
//
//        return tree;
//    }

    private <Type> JSONObjectBase unwrapObject(TypeMeta.Complex<Type> type, Type o) throws JSONException {
        String valueProperty = Helpers.valueProperty((TypeMeta.Complex) type);
        if (valueProperty != null) {
            TypeMeta.Property<Type, Object, Object> property = type.getProperty(valueProperty);
            TypeMeta<Object> subType = property.getType();
            Object sub = property.getAccessor().get(o, null);
            return buildSub(subType, sub);
        }
        return null;
    }

    private <Type> JSONObjectBase buildSub(TypeMeta<Type> type, Type o) throws JSONException {
        if (type == null) {
            throw new IllegalArgumentException();
        } else if (o == null) {
            return buildNull();
//        } else if (type.isArray()) {
//            return buildArray(type.getComponentType(), (Object[]) o, typeTree);
//        } else if (o instanceof Collection) {
//            return buildCollection((Collection) o, typeTree);
//        } else if (o instanceof Map) {
//            return buildMap((Map) o, typeTree);
        } else if (depth == 0 && ! type.isPrimitive() && ((TypeMeta.Complex<Type>) type).isRootType()) {
            return buildRootObject((TypeMeta.Complex<Type>) type, o);
        } else if (! type.isPrimitive()) {
            JSONObjectBase unwrapped = unwrapObject((TypeMeta.Complex<Type>) type, o);
            if (unwrapped != null) {
                return unwrapped;
            }
            return buildObject((TypeMeta.Complex<Type>) type, o);
        }

        return buildPrimitive((TypeMeta.Primitive<Type>) type, o);
    }

//    private JSONArray buildArray(Class elementType, Object[] array, boolean typeTree) throws
//            JSONException {
//        depth++;
//
//        JSONArray result = new JSONArray();
//        if (array != null) {
//            for (int i = 0; i < array.length; i++) {
//                Object object = array[i];
//                result.add(buildSub(object == null ? null : object.getClass(), object, typeTree));
//            }
//        }
//
//        depth--;
//        return result;
//    }

    private <ParentType, Type> JSONObjectBase buildCollection(TypeMeta.Complex<ParentType> parentType,
            ParentType parent, TypeMeta.Property<ParentType, Object, Type> property) throws JSONException {
        depth++;

        Collection<Object> keys = property.getAccessor().keys(parent);
        if (keys == null) {
            return buildNull();
        }

        JSONArray result = new JSONArray();
        for (Object key : keys) {
            result.add(buildSub(property.getType(), property.getAccessor().get(parent, key)));
        }

        depth--;
        return result;
    }

    private <ParentType, Type> JSONObjectBase buildMap(TypeMeta.Complex<ParentType> parentType,
            ParentType parent, TypeMeta.Property<ParentType, Object, Type> property) throws JSONException {
        depth++;

        Collection<String> keys = (Collection) property.getAccessor().keys(parent);
        if (keys == null) {
            return buildNull();
        }

        JSONObject result = new JSONObject();
        for (String key : keys) {
            result.put(key, buildSub(property.getType(), property.getAccessor().get(parent, key)));
        }

        depth--;
        return result;
    }

//    private JSONObject buildMap(Map<Object, Object> data, boolean typeTree) throws JSONException {
//        depth++;
//
//        JSONObject result = new JSONObject();
//        if (data != null) {
//            for (Map.Entry entry : data.entrySet()) {
//                Class type = entry.getValue() == null ? null : entry.getValue().getClass();
//                result.put(context.toIdentifier(entry.getKey().toString()),
//                        buildSub(type, entry.getValue(), typeTree));//, type);
//            }
//        }
//
//        depth--;
//        return result;
//    }

    private String name(Method m) {
        String name = m.getName().substring(3);
        if (name.length() == 0) {
            return null;
        }
        name = name.substring(0, 1).toLowerCase() + name.substring(1);
        return name;
    }

    private boolean isTransient(Class type, String name) {
        Method m;
        try {
            m = type.getMethod("get" + name.substring(0, 1).toUpperCase() + name.substring(1));
        } catch (NoSuchMethodException ex) {
            m = null;
        } catch (SecurityException ex) {
            throw new RuntimeException(ex);
        }
        Field f;
        try {
            f = type.getDeclaredField(name);
        } catch (NoSuchFieldException ex) {
            f = null;
        } catch (SecurityException ex) {
            throw new RuntimeException(ex);
        }

        if (m != null
                && (m.isAnnotationPresent(XmlElement.class)
                || m.isAnnotationPresent(JSONElement.class))) {
            return false;
        }

        if (f != null
                && (f.isAnnotationPresent(XmlElement.class)
                || f.isAnnotationPresent(JSONElement.class))) {
            return false;
        }

        return (m != null && m.isAnnotationPresent(XmlTransient.class))
                || (f != null && f.isAnnotationPresent(XmlTransient.class));
    }

    private <Type> JSONObject buildObject(TypeMeta.Complex<Type> type, Type data) throws JSONException {
        depth++;

        JSONObject result = new JSONObject();
//        Map<String, Object> values = new HashMap();
//        Map<String, Class> types = new HashMap();
//
//        Method[] methods = type.getMethods();
//        for (int i = 0; i < methods.length; i++) {
//            Method method = methods[i];
//            String methodName = name(method);
//            if (!isTransient(type, methodName) && !method.isAnnotationPresent(XmlTransient.class)
//                    && method.getName().startsWith("get") && method.getParameterTypes().length == 0
//                    && !method.getReturnType().equals(Void.TYPE)
//                    && !"getClass".equals(method.getName())) {
//                String name = null;
//                if (method.isAnnotationPresent(JSONElement.class)) {
//                    name = method.getAnnotation(JSONElement.class).name();
//                    if (!name.equals("##default")) {
//                        name = context.toIdentifier(name);
//                    } else {
//                        name = null;
//                    }
//                }
//                if (method.isAnnotationPresent(XmlElement.class)) {
//                    if (name == null) {
//                        name = method.getAnnotation(XmlElement.class).name();
//                        if (!name.equals("##default")) {
//                            name = context.toIdentifier(name);
//                        } else {
//                            name = null;
//                        }
//                    }
//                }
//                if (name == null) {
//                    name = methodName;
//                    name = context.toIdentifier(name);
//                }
////                XmlElement element = method.getAnnotation(XmlElement.class);
////                String name;
////                if (element != null && !element.name().equals("##default")) {
////                    name = context.toIdentifier(element.name());
////                } else {
////                    name = method.getName().substring(3);
////                    name = name.substring(0, 1).toLowerCase() +
////                            name.substring(1);
////                    name = context.toIdentifier(name);
////                }
//                if (!name.equals("class") && !values.containsKey(name)) {
//                    try {
//                        Object value = data == null ? null : method.invoke(data);
//                        values.put(name, value);
//                        types.put(name, value == null ? method.getReturnType() : value.getClass());
//                    } catch (Exception ex) {
//                        throw new JSONException(ex);
//                    }
//                }
//            }
//        }
//
//        Class c = type;
//        while (c != null) {
//            Field[] fields = c.getDeclaredFields();
//            for (Field field : fields) {
//                if (!isTransient(type, field.getName()) && !field.isAnnotationPresent(XmlTransient.class)) {
//                    String name = null;
//                    if (field.isAnnotationPresent(JSONElement.class)) {
//                        name = field.getAnnotation(JSONElement.class).name();
//                        if (!name.equals("##default")) {
//                            name = context.toIdentifier(name);
//                        } else {
//                            name = null;
//                        }
//                    }
//                    if (field.isAnnotationPresent(XmlElement.class)) {
//                        if (name == null) {
//                            name = field.getAnnotation(XmlElement.class).name();
//                            if (!name.equals("##default")) {
//                                name = context.toIdentifier(name);
//                            } else {
//                                name = null;
//                            }
//                        }
//                    }
//                    if (name == null) {
//                        name = context.toIdentifier(field.getName());
//                    }
//
//                    if (!values.containsKey(name)) {
//                        boolean accessible = field.isAccessible();
//                        field.setAccessible(true);
//                        try {
//                            Object value = data == null ? null : field.get(data);
//                            values.put(name, value);
//                            types.put(name, value == null ? field.getType() : value.getClass());
//                        } catch (Exception ex) {
//                            throw new JSONException(ex);
//                        }
//                        field.setAccessible(accessible);
//                    }
//                }
//            }
//            c = c.getSuperclass();
//        }
//
//        for (String name : values.keySet()) {
//            Object value = values.get(name);
//            Class subType = types.get(name);
//            result.put(name, buildSub(subType, value, typeTree));//, subType);
//        }

        for (TypeMeta.Property<Type, Object, Object> property : type.getProperties()) {
            JSONObjectBase sub;
            switch (property.getRelation()) {
                case Single: {
                    Object value = property.getAccessor().get(data, null);
                    if (value == null) {
                        sub = buildNull();
                    } else {
                        sub = buildSub(property.getType(), value);
                    }
                    break;
                }
                case Collection: {
                    sub = buildCollection(type, data, property);
                    break;
                }
                case Map: {
                    sub = buildMap(type, data, property);
                    break;
                }
                default:
                    throw new IllegalArgumentException(property.getRelation().name());
            }
            result.put(property.getName(), sub);
        }

        depth--;
        return result;
    }

    private <Type> JSONObject buildRootObject(TypeMeta.Complex<Type> type, Type data) throws
            JSONException {
        depth++;

        JSONObject result = new JSONObject();
        String rootName = type.getElementName();
        result.put(rootName, buildObject(type, data));//, type);

        depth--;
        return result;
    }

    private <Type> JSONObjectBase buildNull() {
        return new JSONNull();
    }

    private <Type> JSONObjectBase buildPrimitive(TypeMeta.Primitive<Type> type, Type o) {
        if (o == null) {
            throw new IllegalArgumentException();
        } else if (o instanceof Number) {
            return new JSONNumber((Number) o);
        } else if (o instanceof Date) {
            return new JSONNumber(((Date) o).getTime());
        } else if (o instanceof Boolean) {
            return new JSONBoolean((Boolean) o);
        }
        return new JSONString(o.toString());
    }

}
